
import wx
from geometry import linePointDist, distance
from clipimage import generateMask, applyMask

class ClipWidget(wx.Window):
    def __init__(self, parent, image, *args, **argv):
        super(ClipWidget, self).__init__(parent, -1, *args, **argv)
        
        self.polygon = []

        self.hoveredLine = None
        self.hoveredPoint = None
        
        self.selectedPoint = None
        self.selectTime = None
        
        self.zoom = 1.0
        self.origin = (0,0)

        self.moveOrigin = None

        if image == None:
            image = wx.EmptyImage(1,1)
        self.image = image
        self.scaled = self.image
        self.viewBkg = None
        
        self.nextPoint = None
        self.closed = False
        
        self.oldSize = None
        
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        self.Bind(wx.EVT_CHAR, self.OnChar)
        self.Bind(wx.EVT_SIZE, self.OnSize)


    def setImage(self, image):
        self.image = image
        self.scaled = image
        self.calculateViewBackground()
        self.update()


    def getImage(self):
        return self.image
    

    def getPolygon(self):
        return self.polygon
    
    def setPolygon(self, polygon):
        self.polygon = polygon
        self.closed = len(self.polygon) < 3

    def view(self, x, y):
        return (x / self.zoom - self.origin[0], y / self.zoom - self.origin[1])
        
    def model(self, x, y):
        return ((float(x) + self.origin[0]) * self.zoom, 
                (float(y) + self.origin[1]) * self.zoom)
    
    def inView(self, x, y):
        width, height = self.GetClientSizeTuple()
        x, y = self.view(x, y)
        return x >= 0 and x <= width and y >= 0 and y <= height
        
    def lines(self):
        return enumerate(zip(self.polygon, self.polygon[1:] + self.polygon[:1]))
    
    def draw(self, dc):
        gc = wx.GraphicsContext.Create(dc)

        linePen = gc.CreatePen(wx.Pen(wx.Colour(255, 128, 128), width=2))
        hoverLinePen = gc.CreatePen(wx.Pen(wx.Colour(0, 127, 0, 128), width=3))

        insertPen = gc.CreatePen(wx.Pen(wx.Colour(255, 0, 0, 128), width=2))

        pointBorder = gc.CreatePen(wx.Pen(wx.Colour(0, 127, 0, 128)))
        pointInside = gc.CreateBrush(wx.Brush(wx.Colour(0, 255, 0)))
        
        hoverPointBorder = gc.CreatePen(wx.Pen(wx.Colour(0, 127, 127, 128)))
        hoverPointInside = gc.CreateBrush(wx.Brush(wx.Colour(0, 255, 127)))

        x1, y1, x2, y2 = self.viewRect()
        bitmapPos = self.view(x1, y1)
        if self.closed:
            # make alpha mask
            mask = generateMask(self.polygon, x1, y1, x2, y2, (31, 31, 31))
            
            # convert to imageData
            bmp = applyMask(self.viewBkg, mask.Scale(self.viewBkg.GetWidth(),
                                                     self.viewBkg.GetHeight()))
            gc.DrawBitmap(bmp, bitmapPos[0], bitmapPos[1], 
                          bmp.GetWidth(), bmp.GetHeight())

        else:
            bmp = wx.BitmapFromImage(self.viewBkg)
            gc.DrawBitmap(bmp, bitmapPos[0], bitmapPos[1], 
                          bmp.GetWidth(), bmp.GetHeight())
            gc.SetPen(linePen)
            
            if self.hoveredPoint is None and self.nextPoint is not None:
                a = [self.nextPoint]
            elif self.hoveredPoint == 0:
                a = self.polygon[:1]
            else:
                a = []
            if len(self.polygon) + len(a) > 1:
                gc.StrokeLines([self.view(x, y) for x, y in self.polygon + a])
        
        # draw point markers
        for pos, (x, y) in enumerate(self.polygon):
            if pos == self.hoveredPoint:
                gc.SetPen(hoverPointBorder)
                gc.SetBrush(hoverPointInside)
            else:
                gc.SetPen(pointBorder)
                gc.SetBrush(pointInside)
        
            x, y = self.view(x, y)
            gc.DrawRectangle(x - 4, y - 4, 8.0, 8.0)

        if self.hoveredPoint is None and self.nextPoint is not None:
            gc.SetPen(hoverPointBorder)
            gc.SetBrush(hoverPointInside)
            x, y = self.view(*self.nextPoint)
            gc.DrawRectangle(x - 4, y - 4, 8.0, 8.0)

        elif self.hoveredLine is not None and self.selectedPoint is None:
            pos, (x, y) = self.hoveredLine
            gc.SetPen(hoverPointBorder)
            gc.SetBrush(hoverPointInside)
            gc.DrawRectangle(x - 4, y - 4, 8, 8)
    
    def updateHover(self, x, y, alwaysUpdate=False):
        oldHoverInfo = self.hoveredPoint, self.hoveredLine
        self.hoveredPoint = None
        self.hoveredLine = None
        for pos, (sx, sy) in enumerate(self.polygon):
            sx, sy = self.view(sx, sy)
            if (sx - 5 <= x and sx + 5 >= x and
                sy - 5 <= y and sy + 5 >= y):
                self.hoveredPoint = pos
                break
        else:
            nearLines = []
            for pos, (start, stop) in self.lines():
                start = self.view(*start)
                stop = self.view(*stop)
                dist, side, point = linePointDist(start, stop, (x, y))
                if side == 0 and dist < 10:
                    nearLines.append((dist, pos, point))
            
            nearLines.sort()
            if nearLines:
                self.hoveredLine = nearLines[0][1:3]

        if ((self.hoveredPoint, self.hoveredLine) == oldHoverInfo and
            not alwaysUpdate):
            return
        
        self.update()
    
    def update(self):
        self.Refresh()

    def viewRect(self):
        w, h = self.GetClientSizeTuple()
        
        # Get View port in model coordinates
        (x1, y1) = self.model(0, 0)
        (x2, y2) = self.model(w, h)
        
        # Clamp ranges
        return (max(0, int(x1)), max(0, int(y1)), 
                min(self.image.GetWidth(), int(x2) + 1),
                min(self.image.GetHeight(), int(y2) + 1))

    def calculateViewBackground(self):
        if self.scaled:
            x1, y1, x2, y2 = [x / self.zoom for x in self.viewRect()]
            self.viewBkg = self.scaled.GetSubImage(wx.Rect(x1, y1, 
                                                           x2 - x1, y2 - y1))
        else:
            x1, y1, x2, y2 = self.viewRect()
            subImage = self.image.GetSubImage(wx.Rect(x1, y1, x2 - x1, y2 - y1))
            self.viewBkg = subImage.Scale(subImage.GetWidth() / self.zoom,
                                          subImage.GetHeight() / self.zoom)
    
    def OnPaint(self, ev):
        dc = wx.PaintDC(self)
        self.draw(dc)

    def OnSize(self, ev):
        w, h = self.GetClientSizeTuple()
        if self.oldSize is None:
            fkt = max(self.image.GetWidth() / w, self.image.GetHeight() / h)
            for i in range(5):
                self.zoom = 2**i
                if fkt < self.zoom:
                    break
            self.origin = (-(w - self.image.GetWidth() / self.zoom) / 2,
                           -(h - self.image.GetHeight() / self.zoom) / 2)
            self.changeZoom(0)
            self.oldSize = (w, h)
            return

        oW, oH = self.oldSize        
        self.origin = (self.origin[0] + (oW - w) / 2,
                       self.origin[1] + (oH - h) / 2)
        self.oldSize = (w, h)
        
        self.calculateViewBackground()
        self.update()
        
    def OnMotion(self, ev):
        x = ev.GetX()
        y = ev.GetY()
        
        if self.moveOrigin:
            oldX, oldY = self.moveOrigin
            self.moveOrigin = (x, y)
            self.origin = (self.origin[0] - x + oldX, self.origin[1] - y + oldY)
            self.calculateViewBackground()
            self.update()
            return

        if self.closed:
            if self.selectedPoint is not None:
                self.polygon[self.selectedPoint] = self.model(x, y)
                self.updateHover(x, y, True)
            else:
                self.updateHover(x, y)
                
        else:
            if self.selectedPoint is not None:
                self.polygon[self.selectedPoint] = self.model(x, y)
            self.nextPoint = self.model(x, y)
            self.updateHover(x, y, True)
    
    def OnRightDown(self, ev):
        self.moveOrigin = (ev.GetX(), ev.GetY())

    def OnRightUp(self, ev):
        self.moveOrigin = None
        
    def OnLeftDown(self, ev):
        if ev.ControlDown():
            self.moveOrigin = (ev.GetX(), ev.GetY())
            return
        
        if self.hoveredPoint is not None:
            if self.hoveredPoint == 0 and not self.closed:
                self.closed = True
                self.nextPoint = None
                
            else:    
                self.selectedPoint = self.hoveredPoint
                self.selectTime = ev.GetTimestamp()
            
        elif (self.closed and self.hoveredLine is not None and 
              self.selectedPoint is None):
            pos, point = self.hoveredLine
            self.polygon.insert(pos + 1, self.model(*point))
            self.selectedPoint = pos + 1
            self.selectTime = ev.GetTimestamp()
            
        self.updateHover(ev.GetX(), ev.GetY())
    
    def OnLeftUp(self, ev):
        if self.moveOrigin:
            self.moveOrigin = None
            return

        if (self.selectTime is not None and 
            (ev.GetTimestamp() - self.selectTime) < 200):
            del self.polygon[self.selectedPoint]
            
        if self.closed:
            self.selectTime = None
            self.selectedPoint = None
        
        else:
            if self.selectedPoint:
                self.selectedPoint = None
            else:
                self.polygon.append(self.nextPoint)
                self.nextPoint = self.model(ev.GetX(), ev.GetY())
            
        self.updateHover(ev.GetX(), ev.GetY())

    def OnMouseWheel(self, ev):
        self.changeZoom(-ev.GetWheelRotation() / ev.GetWheelDelta())

    def changeZoom(self, exp):
        w, h = self.GetClientSize()
        centerX, centerY = self.model(w / 2, h / 2)
        self.zoom *= 2**exp
        centerX, centerY = self.view(centerX, centerY)
        self.origin = (self.origin[0] + centerX - w/2, 
                       self.origin[1] + centerY - h/2)
        if self.zoom == 1:
            self.scaled = self.image
        elif self.zoom > 1:
            self.scaled = self.image.Scale(self.image.GetWidth() / self.zoom,
                                           self.image.GetHeight() / self.zoom)
        else:
            self.scaled = None
        self.calculateViewBackground()
        self.update()

    def OnChar(self, ev):
        if ev.GetKeyCode() == ord('-'):
            if self.zoom > 2**4:
                return
            self.changeZoom(+1)
            
        elif ev.GetKeyCode() == ord('+'):
            if self.zoom < 2**-4:
                return
            self.changeZoom(-1)
            
        else:
            ev.Skip()

